/**
 * 
 */
package edu.umd.clip.lm.questions;

import java.io.Serializable;
import java.util.*;
import java.util.regex.*;

import org.w3c.dom.*;

import edu.umd.clip.lm.factors.*;
import edu.umd.clip.lm.model.data.Context;
import edu.umd.clip.lm.util.*;
import edu.umd.clip.lm.model.*;

/**
 * @author Denis Filimonov <den@cs.umd.edu>
 *
 */
public class InSetQuestion extends Question {
	public static final String XML_SET_NAME = "set";
	public static final String XML_SET_ITEM_NAME = "item";
	public static final String XML_SET_ITEMS_NAME = "items";
	private static final Pattern split_re = Pattern.compile(",");
	//private HashSet<Integer> set;
	private WordContainer values;
	private static final int ARRAY_CONTAINER_LIMIT = 32;
	
	private interface WordContainer extends Serializable {
		public boolean containsKey(int key);
		public int[] values();
	}
	
	private static class HashContainer implements WordContainer {
		private static final long serialVersionUID = 1L;
		private final CompactReadOnlyIntHashSet values;
		
		public HashContainer(int values[]) {
			this.values = new CompactReadOnlyIntHashSet(values, 1.0f);
		}
		/* (non-Javadoc)
		 * @see edu.umd.clip.lm.questions.InSetQuestion.WordContainer#hasKey(int)
		 */
		@Override
		public boolean containsKey(int key) {
			return values.containsKey(key);
		}

		/* (non-Javadoc)
		 * @see edu.umd.clip.lm.questions.InSetQuestion.WordContainer#getKeys()
		 */
		@Override
		public int[] values() {
			return values.toArray();
		}
	}
	
	private static class ArrayContainer implements WordContainer {
		private static final long serialVersionUID = 1L;
		private final int[] values;
		
		public ArrayContainer(int values[]) {
			this.values = values;
		}
		/* (non-Javadoc)
		 * @see edu.umd.clip.lm.questions.InSetQuestion.WordContainer#hasValue(int)
		 */
		@Override
		public boolean containsKey(int key) {
			return Arrays.binarySearch(values, key) >= 0;
		}

		/* (non-Javadoc)
		 * @see edu.umd.clip.lm.questions.InSetQuestion.WordContainer#values()
		 */
		@Override
		public int[] values() {
			return values;
		}
		
	}
	
	private static WordContainer makeContainer(int values[]) {
		if (values.length > ARRAY_CONTAINER_LIMIT) {
			return new HashContainer(values);
		} else {
			Arrays.sort(values);
			return new ArrayContainer(values); 
		}
		
	}
	public InSetQuestion(byte timeOffset, byte index, Set<Integer> set) {
		super(timeOffset, false);
		this.index = index;
		int values[] = new int[set.size()];
		int i=0;
		for(Integer val : set) {
			values[i++] = val.intValue();
		}
		
		this.values = makeContainer(values);
	}

	public InSetQuestion(Element xmlnode) {
		super(xmlnode);
		Element itemsElement = XMLUtils.getFirstElementByTagName(xmlnode, XML_SET_ITEMS_NAME);
		if (itemsElement != null) {
			String strItems[] = split_re.split(itemsElement.getTextContent());
			int values[] = new int[strItems.length];
			for(int i=0; i<values.length; ++i) {
				values[i] = Integer.parseInt(strItems[i]);
			}
			this.values = makeContainer(values);
		} else {
			Element setElement = XMLUtils.getFirstElementByTagName(xmlnode, XML_SET_NAME);
			NodeList nodes = XMLUtils.getChildrenElementsByTagName(setElement, XML_SET_ITEM_NAME);
			int values[] = new int[nodes.getLength()];
			for(int i=0; i<nodes.getLength(); ++i) {
				values[i] = Integer.parseInt(nodes.item(i).getTextContent());
			}
			this.values = makeContainer(values);
		}
		// NOTE: the values are must be sorted!
		//Arrays.sort(values); // they are sorted already
	}

	@Override
	public Element createXML(Document doc) {
		Element element = super.createXML(doc);
		Element itemsElement = doc.createElement(XML_SET_ITEMS_NAME);

		// sort values for compatibility with previous versions
		int values[] = this.values.values().clone();
		Arrays.sort(values);
		
		if (values.length > 0) {
			StringBuilder sb = new StringBuilder(values.length*7);
			sb.append(values[0]);
			for(int i=1; i<values.length; ++i) {
				sb.append(',');
				sb.append(values[i]);
			}
			itemsElement.setTextContent(sb.toString());
		}
		element.appendChild(itemsElement);
		return element;
	}

	public static Question fromXML(Element xmlnode) {
		return new InSetQuestion(xmlnode);
	}

	@Override
	public int getQuestionType() {
		return IN_SET_QUESTION;
	}

	@Override
	public String toShortString() {
		final int maxWords = 10;
		
		int values[]  = this.values.values();
		
		String words[] = new String[values.length > maxWords ? maxWords + 1 : values.length];
		FactorTupleDescription desc = Experiment.getInstance().getTupleDescription();
		edu.umd.clip.lm.factors.Dictionary dict = desc.getDictionary(index);
		
		int limit = words.length < values.length ? words.length - 1 : values.length;
		for(int i=0; i<limit; ++i) {
			words[i] = dict.getWord(values[i]);
		}
		if (limit < words.length) {
			words[words.length-1] = "...";
		}
		return super.toShortString() + ' ' + Arrays.toString(words);
	}

	@Override
	public String toString() {
		int values[]  = this.values.values();
		String words[] = new String[values.length];
		FactorTupleDescription desc = Experiment.getInstance().getTupleDescription();
		edu.umd.clip.lm.factors.Dictionary dict = desc.getDictionary(index);
		int i = 0;
		for(int w : values) {
			words[i++] = dict.getWord(w);
		}
		String s = "[InSetQuestion time="+Integer.toString(timeOffset)+
			" idx=" + Byte.toString(index) + " values="+Arrays.toString(words)+"]";
		return s;
	}

	/* (non-Javadoc)
	 * @see edu.umd.clip.lm.questions.Question#implication(edu.umd.clip.lm.questions.Question, boolean)
	 */
	@Override
	public Boolean implication(Question q, boolean value) {
		// not worth testing for implications
		return null;
	}

	/* (non-Javadoc)
	 * @see edu.umd.clip.lm.questions.Question#test(edu.umd.clip.lm.model.training.Context)
	 */
	@Override
	public boolean test(Context ctx) {
		long tuple = ctx.data[ctx.data.length + getTimeOffset()];
		int val = FactorTuple.getValue(tuple, index);
		return values.containsKey(val);
	}

	/* (non-Javadoc)
	 * @see edu.umd.clip.lm.questions.Question#test(java.util.AbstractList)
	 */ 
	@Override
	public TestResult test(AbstractProbTree[] context) {
		AbstractProbTree tree = context[-(1 + getTimeOffset())];
		int val = FactorTuple.getValue(tree.getTuple(), index); 
		return new TestResult(values.containsKey(val));
	} 

	/* (non-Javadoc)
	 * @see edu.umd.clip.lm.questions.Question#test(java.util.AbstractList)
	 */ 
	@Override
	public TestResult test(AbstractProbTree tree) {
		int val = FactorTuple.getValue(tree.getTuple(), index); 
		return new TestResult(values.containsKey(val));
	} 

	public int[] getValues() {
		return values.values();
	}

	public boolean hasValue(int value) {
		return values.containsKey(value);
	}
	
}
